package com.listeningframework.boot.autoconfigure.orm.mybatis;

import com.google.common.collect.Lists;
import com.listeningframework.boot.BootApplication;
import com.listeningframework.boot.autoconfigure.orm.mybatis.annotation.MyBatisEntity;
import com.listeningframework.boot.autoconfigure.orm.mybatis.annotation.MyBatisRepository;
import com.listeningframework.boot.autoconfigure.orm.mybatis.dialect.MySqlDialect;
import com.listeningframework.boot.autoconfigure.orm.mybatis.plugins.MyBatisDialectPlugin;
import com.listeningframework.boot.commons.utils.CollectionUtils;
import org.apache.ibatis.io.ResolverUtil;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/***

 Mybatis Auto Configuration
  * <p>
  * 1. Ignore @MapperScan(annotationClass = MyBatisRepository.class, basePackages = {
  * LISTENING_BOOT_PACKAGE, LISTENING_WORKS_PACKAGE })
  * Annotation, Use Method mapperScannerConfigurer() Instead
  * </p>
  * @author listening
 * @version $Id: MybatisAutoConfiguration.java, v 0.1 2015年3月27日 下午10:00:16 listening Exp $
 */
@Configuration
@ConditionalOnClass({SqlSessionFactoryBean.class, MapperScannerConfigurer.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
@EnableConfigurationProperties({MyBatisProperties.class})
public class MyBatisConfiguration implements BeanFactoryAware {
    /**
     * Location Delimiter
     */
    private static final String LOCATION_DELIMITER = ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS;
    /**
     * Default Mapper XML Resources Location
     */
    private static final String MAPPER_RESOURCES = "classpath*:spring/mybatis/**/*Mapper.xml";

    private ConfigurableListableBeanFactory beanFactory;

    @Bean
    @ConditionalOnMissingBean(MapperScannerConfigurer.class)
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScanner = new MapperScannerConfigurer();
        mapperScanner.setAnnotationClass(MyBatisRepository.class);
        mapperScanner.setBasePackage(getConfigLocation());
        return mapperScanner;
    }

    /**
     * MyBatis SqlSessionFactoryBean
     *
     * @param dataSource 数据源
     * @param properties mybatis配置参数
     * @return SqlSessionFactoryBean
     * @throws IOException 处理异常
     */
    @Bean
    @ConditionalOnMissingBean(SqlSessionFactoryBean.class)
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource, MyBatisProperties properties) throws IOException {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        Set<Class<? extends Class<?>>> classSet = findEntityClass(getConfigLocation());
        sessionFactory.setTypeAliases(classSet.toArray(new Class<?>[classSet.size()]));
        sessionFactory.setMapperLocations(parseClasspathResources(MAPPER_RESOURCES));
        setMyBatisPlugins(sessionFactory, properties);

        org.apache.ibatis.session.Configuration mybatisConfig = new org.apache.ibatis.session.Configuration();
        if (properties.isShowSql()) {
            mybatisConfig.setLogPrefix("listening.mybatis.");
        }
        sessionFactory.setConfiguration(mybatisConfig);
        return sessionFactory;
    }

    /**
     * TransactionManager
     *
     * @param dataSource 数据源
     * @return DataSourceTransactionManager
     */
    @Bean(name = "transactionManager")
    @ConditionalOnMissingBean(DataSourceTransactionManager.class)
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    /**
     * Set Mybatis Plugins, For Example, common pagination and SQL log print
     */
    private void setMyBatisPlugins(SqlSessionFactoryBean sessionFactory, MyBatisProperties properties) {
        List<Interceptor> plugins = new ArrayList<Interceptor>();
        // MyBatis数据库方言设置
        MyBatisDialectPlugin mybatisDialectPlugin = new MyBatisDialectPlugin();
        mybatisDialectPlugin.setDialect(new MySqlDialect());
        plugins.add(mybatisDialectPlugin);
        sessionFactory.setPlugins(plugins.toArray(new Interceptor[plugins.size()]));
    }

    /**
     * Parse The ClassPath Resources
     *
     * @param expression
     * @return Resource[]
     * @throws IOException
     */
    private Resource[] parseClasspathResources(String expression) throws IOException {
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        return resolver.getResources(MAPPER_RESOURCES);
    }

    /**
     * Scan MyBatis Entity
     *
     * @return Set<Class<? extends Class<?>>>
     */
    private Set<Class<? extends Class<?>>> findEntityClass(String path) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
        String[] typeAliasPackageArray = tokenizeToUniqueArray(path);
        resolverUtil.findAnnotated(MyBatisEntity.class, typeAliasPackageArray);
        return resolverUtil.getClasses();
    }

    /**
     * Tokenize To Unique Array
     *
     * @param value
     * @return String[]
     */
    private String[] tokenizeToUniqueArray(String value) {
        return CollectionUtils.tokenizeToUniqueArray(value, LOCATION_DELIMITER);
    }

    /**
     * Get Config && Scanner Path， mybatis will scan the path
     */
    private final String getConfigLocation() {
        List<String> packages = new ArrayList<String>();
        // Step 1. Get Project Packages
        List<String> basePackages = Lists.newArrayList();
        if (AutoConfigurationPackages.has(this.beanFactory)) {
            basePackages = AutoConfigurationPackages.get(this.beanFactory);
        }
        if (!CollectionUtils.isEmpty(basePackages)) {
            packages.addAll(basePackages);
        }
        // Step 2. If Projects Packages Is Empty Add Defaults
        if (CollectionUtils.isEmpty(packages)) {
            packages.add(BootApplication.LISTENING_BOOT);
        }
        // Step 3. Convert To String
        StringBuilder builder = new StringBuilder();
        for (String value : packages) {
            builder.append(value).append(";");
        }
        String fullString = builder.toString();
        return fullString.substring(0, fullString.length() - 1);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }
}
